#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)

# This file is part of Cockpit.
#
# Copyright (C) 2015 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <https://www.gnu.org/licenses/>.

import math
import os
import unittest

import storagelib
import testlib


# We use basename a lot, let's reduce the noise
def bn(path):
    return os.path.basename(path)


@testlib.nondestructive
class TestStorageLvm2(storagelib.StorageCase):

    def can_do_layouts(self):
        return self.storaged_version >= [2, 10]

    def skip_if_no_layouts(self):
        if not self.can_do_layouts():
            raise unittest.SkipTest("raid layouts not supported")

    def testLvm(self):
        m = self.machine
        b = self.browser

        mount_point_one = "/run/one"
        mount_point_thin = "/run/thin"

        self.login_and_go("/storage")

        dev_1 = self.add_ram_disk()
        dev_2 = self.add_loopback_disk(name="loop10")
        b.wait_visible(self.card_row("Storage", name=dev_1))
        b.wait_visible(self.card_row("Storage", name=dev_2))

        # Create a volume group out of two disks
        m.execute(f"vgcreate TEST1 {dev_1} {dev_2}")
        # just in case the test fails
        self.addCleanupVG("TEST1")
        self.click_card_row("Storage", name="TEST1")
        b.wait_visible(self.card_row("LVM2 volume group", name=dev_1))
        b.wait_visible(self.card_row("LVM2 volume group", name=dev_2))

        # Create two logical volumes
        m.execute("lvcreate TEST1 -n one -L 20m")
        b.wait_visible(self.card_row("LVM2 logical volumes", name="one"))
        m.execute("lvcreate TEST1 -n two -L 20m")
        b.wait_visible(self.card_row("LVM2 logical volumes", name="two"))

        # Deactivate one
        m.execute("lvchange TEST1/one -a n")
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "Inactive logical volume")

        # and remove it
        m.execute("until lvremove -f TEST1/one; do sleep 5; done")
        b.wait_not_present(self.card_row("LVM2 logical volumes", name="one"))

        # remove a disk from the volume group
        m.execute(f"pvmove {dev_2} &>/dev/null || true")
        m.execute(f"vgreduce TEST1 {dev_2}")
        b.wait_not_present(self.card_row("LVM2 volume group", name=dev_2))

        # The remaining lone disk is not removable
        b.click(self.dropdown_toggle(self.card_row("LVM2 volume group", name=dev_1)))
        b.wait_visible(self.dropdown_action(self.card_row("LVM2 volume group", name=dev_1), "Remove") + "[disabled]")
        b.wait_text(self.dropdown_description(self.card_row("LVM2 volume group", name=dev_1), "Remove"),
                    "Last cannot be removed")

        # Wipe the disk and make sure lvmetad forgets about it.  This
        # might help with reusing it in the second half of this test.
        #
        # HACK - the pvscan is necessary because of
        # https://bugzilla.redhat.com/show_bug.cgi?id=1063813
        #
        m.execute(f"wipefs -a {dev_2}")
        m.execute(f"pvscan --cache {dev_2}")

        # Thin volumes
        m.execute("lvcreate TEST1 --thinpool pool -L 20m")
        b.wait_visible(self.card_row("LVM2 logical volumes", name="pool"))
        m.execute("lvcreate -T TEST1/pool -n thin -V 100m")
        b.wait_visible(self.card_row("LVM2 logical volumes", name="thin"))
        self.click_card_row("LVM2 logical volumes", name="pool")
        b.wait_visible(self.card_row("Thinly provisioned LVM2 logical volumes", name="thin"))

        m.execute("dd if=/dev/urandom of=/dev/mapper/TEST1-thin bs=1M count=10 status=none")
        b.wait_text(self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"), "50%")
        m.execute("until lvremove -f TEST1/thin; do sleep 5; done")
        b.wait_text(self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"), "0%")

        # remove the volume group
        b.go("#/")
        m.execute("vgremove -f TEST1")
        b.wait_visible(self.card("Storage"))
        b.wait_not_present(self.card_row("Storage", name="TEST1"))

        # create volume group in the UI

        self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                               expect=lambda: (self.dialog_is_present('disks', dev_1) and
                                               self.dialog_is_present('disks', dev_2) and
                                               self.dialog_check({"name": "vgroup0"})),
                               values={"disks": {dev_1: True,
                                                 dev_2: True}})

        # just in case the test fails
        self.addCleanupVG("vgroup0")
        b.wait_visible(self.card_row("Storage", name="vgroup0"))

        # Check that the next name is "vgroup1"
        self.click_devices_dropdown("Create LVM2 volume group")
        self.dialog_wait_open()
        self.dialog_wait_val("name", "vgroup1")
        self.dialog_cancel()
        self.dialog_wait_close()

        self.click_card_row("Storage", name="vgroup0")

        # create a logical volume
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block",
                            "size": 20})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")

        # check that the next default name is "lvol1"
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog_wait_open()
        self.dialog_wait_val("name", "lvol1")
        self.dialog_cancel()
        self.dialog_wait_close()

        # rename lvol0
        self.click_card_row("LVM2 logical volumes", 1)
        b.click(self.card_desc_action("LVM2 logical volume", "Name"))
        self.dialog_wait_open()
        self.dialog({"name": "lvol1"})
        b.wait_text(self.card_desc("LVM2 logical volume", "Name"), "lvol1")

        if self.can_do_layouts():
            # check that it is stored on dev_2
            b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(dev_2))

        # grow it
        b.click(self.card_button("LVM2 logical volume", "Grow"))
        self.dialog({"size": 30})

        # format and mount it
        self.click_card_dropdown("Unformatted data", "Format")
        self.dialog({"type": "ext4",
                     "mount_point": mount_point_one})
        self.wait_mounted("ext4 filesystem")
        self.assert_in_configuration("/dev/vgroup0/lvol1", "fstab", "dir", mount_point_one)
        b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), mount_point_one)

        # unmount it
        b.click(self.card_button("ext4 filesystem", "Unmount"))
        self.confirm()
        self.wait_not_mounted("ext4 filesystem")

        # shrink it, this time with a filesystem.
        b.click(self.card_button("LVM2 logical volume", "Shrink"))
        self.dialog({"size": 10})

        # delete it
        self.click_card_dropdown("LVM2 logical volume", "Delete")
        self.confirm()
        b.wait_visible(self.card("LVM2 logical volumes"))
        b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol1"))
        self.assertEqual(m.execute(f"grep {mount_point_one} /etc/fstab || true"), "")

        # remove disk2
        self.click_dropdown(self.card_row("LVM2 volume group", name=dev_2), "Remove")
        b.wait_not_present(self.card_row("LVM2 volume group", name=dev_2))
        b.wait_in_text(self.card_desc("LVM2 volume group", "Capacity"), "50.3 MB")

        # create thin pool and volume
        # the pool will be maximum size, 50.3 MB
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog(expect={"size": 50.3},
                    values={"purpose": "pool",
                            "name": "pool",
                            "size": 38})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "pool")

        self.click_dropdown(self.card_row("LVM2 logical volumes", 1),
                            "Create new thinly provisioned logical volume")
        self.dialog(expect={"name": "lvol0"},
                    values={"name": "thin",
                            "size": 50})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "thin")

        # add a disk and resize the pool
        b.click(self.card_button("LVM2 volume group", "Add physical volume"))
        self.dialog_wait_open()
        self.dialog_set_val('disks', {dev_2: True})
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_visible(self.card_row("LVM2 volume group", name=dev_2))
        # this is sometimes 96, sometimes 100 MB
        cap = self.card_desc("LVM2 volume group", "Capacity")
        b.wait_js_func("(sel => { const c = Number(ph_text(sel).split(' ')[0]); return c >= 96 && c <= 101 })", cap)

        self.click_card_row("LVM2 logical volumes", name="pool")
        b.click(self.card_button("Pool for thinly provisioned LVM2 logical volumes", "Grow"))
        self.dialog({"size": 70})

        # use almost all of the pool by erasing the thin volume
        self.click_card_row("Thinly provisioned LVM2 logical volumes", 1)
        self.click_card_dropdown("Unformatted data", "Format")
        self.dialog({"erase.on": True,
                     "type": "ext4",
                     "mount_point": mount_point_thin})
        self.assert_in_configuration("/dev/vgroup0/thin", "fstab", "dir", mount_point_thin)
        b.click(self.card_parent_link())
        b.wait_text(self.card_row_col("Thinly provisioned LVM2 logical volumes", 1, 3), mount_point_thin)

        # prevent unstable usage numbers from failing the pixel test
        b.assert_pixels('body', "pool-page", mock={".usage-text": "1 / 100 MB"},
                        ignore=[".usage-bar"])

        # remove pool
        self.click_card_dropdown("Pool for thinly provisioned LVM2 logical volumes", "Delete")
        self.confirm()
        b.wait_visible(self.card("LVM2 logical volumes"))
        b.wait_not_present(self.card_row("LVM2 logical volumes", name="pool"))
        self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")

        # make another logical volume and format it, just so that we
        # can see whether deleting the volume group will clean it all
        # up.
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block"})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
        self.click_card_row("LVM2 logical volumes", 1)
        self.click_card_dropdown("Unformatted data", "Format")
        self.dialog({"type": "ext4",
                     "mount_point": mount_point_one})
        self.assert_in_configuration("/dev/vgroup0/lvol0", "fstab", "dir", mount_point_one)
        b.click(self.card_parent_link())
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem")
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), mount_point_one)

        b.wait_visible(self.card_row_col("LVM2 logical volumes", 1, 4) + " .usage-bar[role=progressbar]")
        b.assert_pixels('body', "page",
                        # Usage numbers are not stable and also cause
                        # the table columns to shift. The usage bars
                        # are not stable but are always the same size,
                        # so it is good enough to ignore them.
                        mock={self.card_desc("LVM2 volume group", "UUID"): "a12978a1-5d6e-f24f-93de-11789977acde",
                              ".usage-text": "---"},
                        ignore=[".usage-bar"])

        # rename volume group
        b.click(self.card_desc_action("LVM2 volume group", "Name"))
        self.dialog_wait_open()
        self.dialog_set_val("name", "vgroup1")
        self.dialog_apply()
        self.dialog_wait_close()

        # remove volume group
        self.click_card_dropdown("LVM2 volume group", "Delete")
        self.confirm()
        b.wait_visible(self.card("Storage"))
        b.wait_not_present(self.card_row("Storage", name="vgroup1"))
        self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")

    def testUnpartitionedSpace(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk()
        b.wait_visible(self.card_row("Storage", name=disk1))
        b.wait_visible(self.card_row("Storage", name=disk2))

        # Put a partition table on disk1 and disk2
        m.execute(f'parted -s {disk1} mktable gpt')
        m.execute(f'parted -s {disk2} mktable gpt')

        # Create a volume group out of disk1
        self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown('Create LVM2 volume group'),
                               expect=lambda: self.dialog_is_present('disks', disk1) and
                               self.dialog_is_present('disks', "unpartitioned space on Linux scsi_debug"),
                               values={"disks": {disk1: True}})

        self.click_card_row("Storage", name="vgroup0")

        # Check the we are really using a partition on disk1 now
        b.wait_in_text(self.card_row_col("LVM2 volume group", 1, 2), "Partition - Linux scsi_debug")

        # Add the unused space of disk2
        self.dialog_with_retry(trigger=lambda: b.click(self.card_button("LVM2 volume group",
                                                                        "Add physical volume")),
                               expect=lambda: self.dialog_is_present(
                                   'disks', "unpartitioned space on " + disk2),
                               values={"disks": {disk2: True}})
        b.wait_in_text(self.card_row_col("LVM2 volume group", 1, 2), "Partition - Block device")

    def testSnapshots(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        disk = self.add_ram_disk(100)
        b.wait_visible(self.card_row("Storage", name=disk))

        m.execute(f"vgcreate TEST {disk}")

        self.addCleanupVG("TEST")
        self.click_card_row("Storage", name="TEST")

        # Create a thinpool and a thin volume in it

        m.execute("lvcreate TEST --thinpool pool -L 10m")
        m.execute("lvcreate -T TEST/pool -n thin -V 30m")
        b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "thin")

        # Create a logical volume and take a snapshot of it.  We will
        # later check that the snapshot isn't shown.

        m.execute("lvcreate TEST -n lvol0 -L 10m")
        m.execute("lvcreate -s -n snap0 -L 10m TEST/lvol0")

        # the above lvol0 will be the new first entry in the table, so
        # TEST/thin moves to he third row
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
        b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "pool")
        b.wait_text(self.card_row_col("LVM2 logical volumes", 3, 1), "thin")

        # Take a snapshot of the thin volume and check that it appears

        self.click_dropdown(self.card_row("LVM2 logical volumes", 3), "Create snapshot")
        self.dialog({"name": "mysnapshot"})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 3, 1), "mysnapshot")

        # Now check that the traditional snapshot is not shown.  We do
        # this here to be sure that Cockpit is fully caught up and has
        # actually ignored it instead of just not having gotten around
        # to showing it.

        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
        b.wait_not_present(self.card_row("LVM2 logical volumes", name="snap0"))

    def testRaid(self):
        b = self.browser

        self.skip_if_no_layouts()

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk(name="loop10")
        disk3 = self.add_loopback_disk(name="loop11")
        disk4 = self.add_loopback_disk(name="loop12")

        # Make a volume group with four physical volumes

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True}})

        self.addCleanupVG("vgroup0")

        # Make a raid5 on three PVs, using about half of each

        self.click_card_row("Storage", name="vgroup0")
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog_wait_open()
        self.dialog_wait_val("name", "lvol0")
        self.dialog_set_val("purpose", "block")
        self.dialog_set_val("layout", "raid5")
        self.dialog_set_val("pvs", {disk1: False})
        self.dialog_set_val("size", 40)
        b.assert_pixels("#dialog", "create-raid5")
        self.dialog_apply()
        self.dialog_wait_close()

        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
        self.click_card_row("LVM2 logical volumes", 1)

        b.wait_text(self.card_desc("LVM2 logical volume", "Layout"), "Distributed parity (RAID 5)")
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk2))
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk3))
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk4))

        b.assert_pixels(self.card("LVM2 logical volume"), "raid5-card")

        # Make linear volume to fully use second PV

        b.click(self.card_parent_link())
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog(expect={"name": "lvol1"},
                    values={"purpose": "block",
                            "layout": "linear",
                            "pvs": {disk1: False, disk3: False, disk4: False}})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "lvol1")
        self.click_card_row("LVM2 logical volumes", 2)
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk2))

        b.click(self.card_parent_link())
        self.click_card_row("LVM2 logical volumes", 1)

        # Grow raid5 to about maximum
        b.click(self.card_button("LVM2 logical volume", "Grow"))
        self.dialog_wait_open()
        self.dialog_set_val("size", 80)
        b.assert_pixels("#dialog", "grow-raid5")
        self.dialog_apply()
        self.dialog_wait_close()

        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))

        # Check that each PV is used exactly once.
        card = self.card("LVM2 logical volume")
        b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)
        b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)
        b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)
        b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)
        b.assert_pixels(card, "raid5-card2")

    def testBrokenLinear(self):
        b = self.browser

        self.skip_if_no_layouts()

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk()
        disk3 = self.add_loopback_disk()

        # Make a volume group with three physical volumes

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True}})

        self.addCleanupVG("vgroup0")

        # Make a linear volume on two of them

        self.click_card_row("Storage", name="vgroup0")
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block",
                            "layout": "linear",
                            "pvs": {disk2: False}})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
        self.click_card_row("LVM2 logical volumes", 1)

        b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk1))
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk3))

        # Kill one PV

        self.force_remove_disk(disk1)
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"),
                       "This logical volume has lost some of its physical volumes and can no longer be used.")
        b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk1))

        b.click(self.card_parent_link())
        b.wait_in_text(".pf-v5-c-alert", "This volume group is missing some physical volumes.")
        b.wait_visible(self.card_desc_action("LVM2 volume group", "Name") + ":disabled")
        b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-times-circle')

        # Dismiss alert, this will delete the volume

        b.click(".pf-v5-c-alert button:contains(Dismiss)")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "vgroup0/lvol0")
        self.dialog_apply()
        self.dialog_wait_close()

        b.wait_not_present(".pf-v5-c-alert")
        b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))

    def testMaxLayoutSizes(self):
        b = self.browser

        self.skip_if_no_layouts()

        # Make sure that Cockpit gets the computation of the maximum
        # size right.

        self.login_and_go("/storage")

        disk1 = self.add_loopback_disk(name="loop10")
        disk2 = self.add_loopback_disk(name="loop11")
        disk3 = self.add_loopback_disk(name="loop12")
        disk4 = self.add_loopback_disk(name="loop13")
        disk5 = self.add_loopback_disk(name="loop14")
        disk6 = self.add_loopback_disk(name="loop15")

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_is_present('disks', disk5) and
                                                   self.dialog_is_present('disks', disk6) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True,
                                                     disk5: True,
                                                     disk6: True}})
        self.addCleanupVG("vgroup0")
        self.click_card_row("Storage", name="vgroup0")

        def mb(size):
            if size < 100e6:
                return str(round(size / 1e6, 1)) + " MB"
            else:
                return str(round(size / 1e6)) + " MB"

        def test(layout, expected_size):
            b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
            self.dialog_wait_open()
            self.dialog_wait_val("name", "lvol0")
            self.dialog_set_val("purpose", "block")
            self.dialog_set_val("layout", layout)
            if layout == "raid10":
                self.dialog_set_val("pvs", {disk6: False})
                self.dialog_apply()
                self.dialog_wait_error("pvs", "an even number")
                self.dialog_set_val("pvs", {disk6: True})
            self.dialog_apply()
            self.dialog_wait_close()

            b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
            self.click_card_row("LVM2 logical volumes", 1)

            field = "Physical volumes" if layout.startswith("linear") else "Stripes"

            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk1))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk2))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk3))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk4))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk5))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk6))

            b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), mb(expected_size))

            self.click_card_dropdown("LVM2 logical volume", "Delete")
            with b.wait_timeout(60):
                self.confirm()
            b.wait_visible(self.card("LVM2 logical volumes"))
            b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))

        ext_size = 4 * 1024 * 1024
        pv_size = math.floor(50e6 / ext_size) * ext_size
        n_pvs = 6

        test("linear", n_pvs * pv_size)
        test("raid0", n_pvs * pv_size)
        test("raid1", pv_size - ext_size)
        test("raid10", (n_pvs / 2) * (pv_size - ext_size))
        test("raid5", (n_pvs - 1) * (pv_size - ext_size))
        test("raid6", (n_pvs - 2) * (pv_size - ext_size))

    def testMaxLayoutGrowth(self):
        b = self.browser

        self.skip_if_no_layouts()

        # Make sure that Cockpit gets the computation of the maximum
        # size right when growing a logical volume.

        self.login_and_go("/storage")

        disk1 = self.add_loopback_disk(name="loop10")
        disk2 = self.add_loopback_disk(name="loop11")
        disk3 = self.add_loopback_disk(name="loop12")
        disk4 = self.add_loopback_disk(name="loop13")
        disk5 = self.add_loopback_disk(name="loop14")
        disk6 = self.add_loopback_disk(name="loop15")

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_is_present('disks', disk5) and
                                                   self.dialog_is_present('disks', disk6) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True,
                                                     disk5: True,
                                                     disk6: True}})
        self.addCleanupVG("vgroup0")
        self.click_card_row("Storage", name="vgroup0")

        def mb(size):
            if size < 100e6:
                return str(round(size / 1e6, 1)) + " MB"
            else:
                return str(round(size / 1e6)) + " MB"

        def test(layout, expected_size):
            b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
            self.dialog(expect={"name": "lvol0"},
                        values={"purpose": "block",
                                "layout": layout,
                                "size": round(expected_size / 2e6)})

            b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
            self.click_card_row("LVM2 logical volumes", 1)

            field = "Stripes"

            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk1))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk2))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk3))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk4))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk5))
            b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk6))

            # Grow to max with default pvs
            b.click(self.card_button("LVM2 logical volume", "Grow"))
            self.dialog_wait_open()
            slider = self.dialog_field("size") + " .pf-v5-c-slider .pf-v5-c-slider__rail"
            width = b.call_js_func('(function (sel) { return ph_find(sel).offsetWidth; })', slider)
            b.mouse(slider, "click", width, 0)
            self.dialog_apply()
            self.dialog_wait_close()

            card = self.card("LVM2 logical volume")
            b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)
            b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)
            b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)
            b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)
            b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk5)}')", 1)
            b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk6)}')", 1)

            b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), mb(expected_size))

            self.click_card_dropdown("LVM2 logical volume", "Delete")
            with b.wait_timeout(60):
                self.confirm()
            b.wait_visible(self.card("LVM2 logical volumes"))
            b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))

        ext_size = 4 * 1024 * 1024
        pv_size = math.floor(50e6 / ext_size) * ext_size
        n_pvs = 6

        test("raid0", n_pvs * pv_size)
        test("raid1", pv_size - ext_size)
        test("raid10", (n_pvs / 2) * (pv_size - ext_size))
        test("raid5", (n_pvs - 1) * (pv_size - ext_size))
        test("raid6", (n_pvs - 2) * (pv_size - ext_size))

    @testlib.skipImage("No targetd in Arch Linux", "arch")
    def testDegradation(self):
        b = self.browser

        self.skip_if_no_layouts()

        # Make one (very small) logical volume for each RAID type and
        # then break them.

        self.login_and_go("/storage")

        disk1 = self.add_targetd_loopback_disk(index=1, size=100)
        disk2 = self.add_targetd_loopback_disk(index=2, size=100)
        disk3 = self.add_targetd_loopback_disk(index=3, size=100)
        disk4 = self.add_targetd_loopback_disk(index=4, size=100)
        disk5 = self.add_targetd_loopback_disk(index=5, size=100)

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_is_present('disks', disk5) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True,
                                                     disk5: True}})
        self.addCleanupVG("vgroup0")
        self.click_card_row("Storage", name="vgroup0")

        def create(row, layout, expected_name):
            b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
            self.dialog_wait_open()
            self.dialog_wait_val("name", expected_name)
            self.dialog_set_val("layout", layout)
            if layout == "raid10":
                self.dialog_set_val("pvs", {disk5: False})
            elif layout == "linear":
                self.dialog_set_val("pvs", {disk2: False, disk3: False, disk4: False, disk5: False})
            self.dialog_set_val("size", 10)
            self.dialog_apply()
            self.dialog_wait_close()
            b.wait_text(self.card_row_col("LVM2 logical volumes", row, 1), expected_name)

        create(1, "linear", "lvol0")
        create(2, "raid0", "lvol1")
        create(3, "raid1", "lvol2")
        create(4, "raid10", "lvol3")
        create(5, "raid5", "lvol4")
        create(6, "raid6", "lvol5")

        def wait_msg(row, msg):
            self.click_card_row("LVM2 logical volumes", row)
            b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes" if row == 1 else "Stripes"),
                           msg)
            b.click(self.card_parent_link())

        def wait_partial(row):
            wait_msg(row, "This logical volume has lost some of its physical volumes and can no longer be used.")

        def wait_degraded(row):
            wait_msg(row, "This logical volume has lost some of its physical volumes but has not lost any data yet.")

        def wait_maybe_partial(row):
            wait_msg(row, "This logical volume has lost some of its physical volumes but might not have lost any data yet.")

        self.force_remove_disk(disk1)

        wait_partial(1)         # linear is broken now
        wait_partial(2)         # striped as well
        wait_degraded(3)        # mirror is fine
        wait_degraded(4)        # striped mirror as well
        wait_degraded(5)        # raid5 and ...
        wait_degraded(6)        # ... raid6 are doing their job

        self.force_remove_disk(disk2)

        wait_degraded(3)        # mirror is still fine
        wait_maybe_partial(4)   # striped mirror is not sure anymore
        wait_partial(5)         # raid5 is now toast
        wait_degraded(6)        # but raid6 still hangs on

        self.force_remove_disk(disk3)

        wait_degraded(3)        # mirror is _still_ fine
        wait_partial(4)         # striped mirror has lost more than half and is kaputt
        wait_partial(6)         # raid6 is finally toast as well

    def testLvmOnLuks(self):
        b = self.browser
        m = self.machine

        self.skip_if_no_layouts()

        # Make sure that Cockpit gets the layout description right for
        # encrypted physical volumes

        self.login_and_go("/storage")

        disk = self.add_loopback_disk()
        b.wait_visible(self.card_row("Storage", name=disk))
        m.execute(f"echo einszweidrei | cryptsetup luksFormat --pbkdf-memory 32768 {disk}")
        m.execute(f"echo einszweidrei | cryptsetup luksOpen {disk} dm-test")
        self.addCleanup(m.execute, "cryptsetup close dm-test || true")
        m.execute("vgcreate vgroup0 /dev/mapper/dm-test")
        self.addCleanupVG("vgroup0")
        m.execute("lvcreate vgroup0 -n lvol0 -l100%FREE")

        self.click_card_row("Storage", name="lvol0")
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk))


class TestStorageLvm2Destructive(storagelib.StorageCase):

    def can_do_layouts(self):
        return self.storaged_version >= [2, 10]

    def skip_if_no_layouts(self):
        if not self.can_do_layouts():
            raise unittest.SkipTest("raid layouts not supported")

    # This is only "destructive" because of
    #
    #     https://bugzilla.redhat.com/show_bug.cgi?id=2256432
    #
    # We can't revocer the machine after that bug has hit us.

    def testRaidRepair(self):
        b = self.browser
        m = self.machine

        self.skip_if_no_layouts()

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk(name="loop10")
        disk3 = self.add_loopback_disk(name="loop11")
        disk4 = self.add_loopback_disk(name="loop12")

        # Make a volume group with three physical volumes

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True}})

        self.addCleanupVG("vgroup0")

        # Make a raid5 on the three PVs

        self.click_card_row("Storage", name="vgroup0")
        b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block",
                            "layout": "raid5"})
        b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
        self.click_card_row("LVM2 logical volumes", 1)

        b.wait_text(self.card_desc("LVM2 logical volume", "Layout"), "Distributed parity (RAID 5)")
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk2))
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk3))

        # Kill one PV

        self.force_remove_disk(disk1)

        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"),
                       "This logical volume has lost some of its physical volumes but has not lost any data yet.")
        b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))

        b.click(self.card_parent_link())
        b.wait_in_text(".pf-v5-c-alert", "This volume group is missing some physical volumes.")
        b.wait_visible(self.card_desc_action("LVM2 volume group", "Name") + ":disabled")
        b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-exclamation-triangle')

        # Repair with fourth

        self.click_card_row("LVM2 logical volumes", 1)
        b.click(self.card_button("LVM2 logical volume", "Repair"))
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "There is not enough space available")
        self.dialog_cancel()
        self.dialog_wait_close()

        b.click(self.card_parent_link())
        b.click(self.card_button("LVM2 volume group", "Add physical volume"))
        self.dialog_wait_open()
        self.dialog_set_val('disks', {disk4: True})
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_visible(self.card_row("LVM2 volume group", name=disk4))
        self.click_card_row("LVM2 logical volumes", 1)

        b.click(self.card_button("LVM2 logical volume", "Repair"))
        self.dialog_wait_open()
        self.dialog_apply()
        self.dialog_wait_error("pvs", "An additional 46.1 MB must be selected")
        self.dialog_set_val("pvs", {disk4: True})
        self.dialog_apply()
        try:
            self.dialog_wait_close()  # wait for repair to finish
        except testlib.Error:
            # Produce evidence of lvconvert hanging, so that our maughties can match on it
            print(m.execute("for p in $(pgrep lvm); do cat /proc/$p/stack; done"))
            raise

        b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Stripes"),
                           "This logical volume has lost some")
        b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk4))

        # Dismiss alert

        b.click(self.card_parent_link())
        b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0"))
        b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-exclamation-triangle')

        b.click(".pf-v5-c-alert button:contains(Dismiss)")
        self.dialog({})
        b.wait_not_present(".pf-v5-c-alert")


if __name__ == '__main__':
    testlib.test_main()
